﻿using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using DevelopersToolbox.Util;

namespace DevelopersToolbox {
    public partial class HexEditor : UserControl {
        private enum Mode {
            Hex, Ascii
        }

        private int _headerHeight;
        private HexEditorData _data = new HexEditorData(new byte[0]);
        private readonly Pen _blackPen = new Pen(Color.Black);
        private readonly Brush _grayBrush = new SolidBrush(SystemColors.Control);
        private readonly Brush _textBrush = new SolidBrush(Color.Black);
        private int _bytesPerLine = 16;
        private int _offsetWidth;
        private int _byteWidth;
        private int _topVisibleOffset;
        private int _lineHeight;
        private readonly StringFormat _centerFormat = new StringFormat { Alignment = StringAlignment.Center };
        private int _byteAsciiWidth;
        private int _byteSpacingWidth;
        private int _byteRightMargin;
        private Caret _caret;
        private int _caretHeight;
        private int _caretWidth;
        private int _caretLocation; // this is twice the offset to handle hex mode
        private Mode _caretMode;
        public event EventHandler DataChanged;

        [DefaultValue(16)]
        public int BytesPerLine {
            get { return _bytesPerLine; }
            set { _bytesPerLine = value; }
        }

        public byte[] Data {
            get { return _data.ToArray(); }
            set {
                _data = new HexEditorData(value);
                DoDataChanged();
            }
        }

        private void DoDataChanged() {
            if (DataChanged != null) {
                EventArgs eventArgs = new EventArgs();
                DataChanged(this, eventArgs);
            }
            Invalidate();
        }

        public HexEditor() {
            InitializeComponent();
        }

        private void HexEditor_Paint(object sender, PaintEventArgs e) {
            SizeF caretSize = e.Graphics.MeasureString("0123456789abcdef0123456789abcdef", Font);
            int charWidth = (int)Math.Ceiling(caretSize.Width / 32);

            _caretWidth = charWidth;
            _caretHeight = (int)Math.Ceiling(caretSize.Height);

            _lineHeight = (int)Math.Ceiling(e.Graphics.MeasureString("0123456789abcdef", Font).Height);
            _offsetWidth = charWidth * "00000000h:W".Length;
            _byteWidth = charWidth * "00".Length;
            _byteSpacingWidth = charWidth * " ".Length;
            _byteAsciiWidth = charWidth * "a".Length;
            _byteRightMargin = charWidth * 2;
            _headerHeight = (int)Math.Ceiling(e.Graphics.MeasureString("0123456789abcdef", Font).Height);

            CalculateAutoScroll();

            _topVisibleOffset = (-AutoScrollPosition.Y / _lineHeight) * _bytesPerLine;
            if (_topVisibleOffset < 0) {
                _topVisibleOffset = 0;
            }

            PaintHeader(e.Graphics);
            for (int offset = _topVisibleOffset, y = _headerHeight;
                y < Height + _lineHeight && offset < _data.Length;
                offset += _bytesPerLine, y += _lineHeight) {
                PaintLine(e.Graphics, offset, y);
            }
        }

        private void CalculateAutoScroll() {
            SetAutoScrollMargin(_byteAsciiWidth * 4, _lineHeight * 4);

            int totalLines = (int)Math.Ceiling((double)_data.Length / _bytesPerLine);
            int height = _headerHeight + (totalLines * _lineHeight);
            int width = _offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + _byteRightMargin + (_bytesPerLine * _byteAsciiWidth);
            AutoScrollMinSize = new Size(width, height);
        }

        private void PaintLine(Graphics g, int offset, int y) {
            RectangleF rect = new RectangleF(AutoScrollPosition.X, y, _offsetWidth, _lineHeight);
            DrawOffset(g, offset, rect);

            for (int curOffset = offset, i = 0; curOffset < offset + _bytesPerLine && curOffset < _data.Length; curOffset++, i++) {
                int x = AutoScrollPosition.X + _offsetWidth + (i * (_byteWidth + _byteSpacingWidth));
                rect = new RectangleF(x, y, _byteWidth + 4, _lineHeight);
                DrawByte(g, curOffset, rect);
            }
            for (int curOffset = offset, i = 0; curOffset < offset + _bytesPerLine && curOffset < _data.Length; curOffset++, i++) {
                int x = AutoScrollPosition.X + _offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + _byteRightMargin + (i * _byteAsciiWidth);
                rect = new RectangleF(x, y, _byteWidth, _lineHeight);
                DrawByteAscii(g, curOffset, rect);
            }
        }

        private void DrawByteAscii(Graphics g, int offset, RectangleF rect) {
            byte b = _data[offset];
            string str = IsPrintable(b) ? ((char)b).ToString() : ".";
            g.DrawString(str, Font, _textBrush, rect, _centerFormat);
        }

        private void DrawOffset(Graphics g, int offset, RectangleF rect) {
            string str = string.Format("{0:X8}h:", offset);
            g.DrawString(str, Font, _textBrush, rect);
        }

        private void DrawByte(Graphics g, int offset, RectangleF rect) {
            string str = string.Format("{0:X2}", (int)_data[offset]);
            g.DrawString(str, Font, _textBrush, rect);
        }

        private void PaintHeader(Graphics g) {
            g.FillRectangle(_grayBrush, 0, 0, Width, _headerHeight);
            g.DrawLine(_blackPen, 0, _headerHeight, Width, _headerHeight);
            for (int i = 0; i < _bytesPerLine; i++) {
                int x = AutoScrollPosition.X + _offsetWidth + (i * (_byteWidth + _byteSpacingWidth));
                RectangleF rect = new RectangleF(x, 0, _byteWidth, _headerHeight);
                g.DrawString(i.ToString("X"), Font, _textBrush, rect, _centerFormat);
            }
            for (int i = 0; i < _bytesPerLine; i++) {
                int x = AutoScrollPosition.X + _offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + _byteRightMargin + (i * _byteAsciiWidth);
                RectangleF rect = new RectangleF(x, 0, _byteWidth, _headerHeight);
                g.DrawString(i.ToString("X"), Font, _textBrush, rect, _centerFormat);
            }
        }

        private void HexEditor_Scroll(object sender, ScrollEventArgs e) {
            Invalidate();
        }

        private void HexEditor_Enter(object sender, EventArgs e) {
            _caret = new Caret(this, _caretWidth, _caretHeight);
            UpdateCaretPosition();
        }

        private void UpdateCaretPosition() {
            Point loc = GetLocationFromOffset(CaretOffset, CaretMode);
            if (CaretMode == Mode.Hex) {
                loc.X += CaretNibble == 0 ? 0 : _caretWidth;
            }
            _caret.SetPosition(loc.X, loc.Y);
        }

        private Point GetLocationFromOffset(int offset, Mode mode) {
            int y = (-AutoScrollPosition.Y) + _headerHeight + (((offset - _topVisibleOffset) / _bytesPerLine) * _lineHeight) + 1;
            int x;
            int offsetInLine = offset % _bytesPerLine;
            if (mode == Mode.Ascii) {
                x = AutoScrollPosition.X + _offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + _byteRightMargin + (offsetInLine * _byteAsciiWidth) + 4;
            } else if (mode == Mode.Hex) {
                x = AutoScrollPosition.X + _offsetWidth + (offsetInLine * (_byteWidth + _byteSpacingWidth)) + 2;
            } else {
                throw new Exception("Invalid mode");
            }
            return new Point(x, y);
        }

        private void HexEditor_Leave(object sender, EventArgs e) {
            if (_caret != null) {
                _caret.Dispose();
                _caret = null;
            }
        }

        private void HexEditor_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) {
            char ch = Convert.ToChar(e.KeyCode);
            ch = e.Shift ? char.ToUpper(ch) : char.ToLower(ch);
            char lowerChar = char.ToLower(ch);

            switch (e.KeyCode) {
                case Keys.Right:
                    if (CaretMode == Mode.Hex) {
                        CaretLocation++;
                    } else {
                        CaretLocation += 2;
                    }
                    e.IsInputKey = true;
                    return;

                case Keys.Left:
                    if (CaretMode == Mode.Hex) {
                        CaretLocation--;
                    } else {
                        CaretLocation -= 2;
                    }
                    e.IsInputKey = true;
                    return;

                case Keys.Down:
                    CaretLocation += _bytesPerLine * 2;
                    e.IsInputKey = true;
                    return;

                case Keys.Up:
                    CaretLocation -= _bytesPerLine * 2;
                    e.IsInputKey = true;
                    return;

                case Keys.Tab:
                    CaretMode = CaretMode == Mode.Hex ? Mode.Ascii : Mode.Hex;
                    UpdateCaretPosition();
                    e.IsInputKey = true;
                    return;
            }

            if (CaretMode == Mode.Hex) {
                if ((ch >= '0' && ch <= '9') || (lowerChar >= 'a' && lowerChar <= 'f')) {
                    byte d = CaretOffset >= _data.Length ? (byte)0 : _data[CaretOffset];
                    byte num = (byte)((ch >= '0' && ch <= '9') ? ch - '0' : lowerChar - 'a' + 10);
                    if (CaretNibble == 0) {
                        d = (byte)((d & 0x0F) | (num << 4));
                    } else {
                        d = (byte)((d & 0xF0) | (num << 0));
                    }
                    ChangeData(CaretOffset, d);
                    CaretLocation++;
                }
            } else if (CaretMode == Mode.Ascii) {
                if (IsPrintable((byte)ch)) {
                    ChangeData(CaretOffset, (byte)ch);
                    CaretLocation += 2;
                }
            }
        }

        private void ChangeData(int offset, byte b) {
            _data[offset] = b;
            DoDataChanged();
        }

        private int CaretNibble {
            get { return _caretLocation % 2; }
        }

        private int CaretOffset {
            get { return _caretLocation / 2; }
        }

        private int CaretLocation {
            get { return _caretLocation; }
            set {
                _caretLocation = value;
                if (_caretLocation < 0) {
                    _caretLocation = 0;
                }
                if (_caretLocation > _data.Length * 2) {
                    _caretLocation = _data.Length * 2;
                }
                UpdateCaretPosition();
            }
        }

        private Mode CaretMode {
            get { return _caretMode; }
            set {
                _caretMode = value;
                UpdateCaretPosition();
            }
        }

        private static bool IsPrintable(byte b) {
            return b >= ' ' && b <= '~';
        }

        private void HexEditor_MouseClick(object sender, MouseEventArgs e) {

        }

        private void HexEditor_MouseDown(object sender, MouseEventArgs e) {
            HitTestInfo info = HitTest(e.Location);
            CaretMode = info.Location == HitTestLocation.Ascii ? Mode.Ascii : Mode.Hex;
            CaretLocation = (info.Offset * 2) + info.Nibble;
        }

        public HitTestInfo HitTest(Point controlLocation) {
            int x = controlLocation.X - AutoScrollPosition.X;
            int y = controlLocation.Y - AutoScrollPosition.Y;
            HitTestInfo results = new HitTestInfo();

            if (y < _headerHeight) {
                results.Location = HitTestLocation.Header;
                return results;
            }

            int line = (y - _headerHeight) / _lineHeight;
            results.Offset = line * _bytesPerLine;

            if (x < _offsetWidth) {
                results.Location = HitTestLocation.Offset;
                results.Offset = results.Offset;
                return results;
            }

            if (x < _offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + 2) {
                results.Location = HitTestLocation.Hex;
                x = x - _offsetWidth - 2;
                int offsetInLine = x / (_byteWidth + _byteSpacingWidth);
                int startOfByteX = offsetInLine * (_byteWidth + _byteSpacingWidth);
                results.Nibble = (x - startOfByteX) / (_byteWidth / 2);
                if (results.Nibble > 1) {
                    results.Nibble = 1;
                }
                results.Offset = results.Offset + offsetInLine;
                return results;
            }

            if (x < _offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + 2 + _byteRightMargin) {
                results.Location = HitTestLocation.Other;
                return results;
            }

            if (x < _offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + _byteRightMargin + (_bytesPerLine * _byteAsciiWidth) + 4) {
                results.Location = HitTestLocation.Ascii;
                x = x - (_offsetWidth + (_bytesPerLine * (_byteWidth + _byteSpacingWidth)) + _byteRightMargin) - 4;
                results.Offset = results.Offset + (x / _byteAsciiWidth);
                return results;
            }

            results.Location = HitTestLocation.Other;
            return results;
        }

        public enum HitTestLocation {
            Header,
            Offset,
            Hex,
            Ascii,
            Other
        }

        public class HitTestInfo {
            public HitTestLocation Location { get; set; }
            public int Offset { get; set; }
            public int Nibble { get; set; }
        }
    }
}
